iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0
Rust

Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計系列 第 25

(Day25) Rust 模組與可見性:縮小 API 表面,封裝不變式

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250917/20124462KA2M7PfuNm.png

Rust 逼我成為更好的工程師:模組與可見性:縮小 API 表面,封裝不變式

在前面的篇章中,我們學會了如何用型別系統確保記憶體安全。

今天我們要探討另一個關鍵問題:如何設計模組邊界,讓錯誤的使用方式變得不可能

把握一個關鍵:公開的 API 越少,維護的負擔越小,使用者犯錯的機會也越少

可見性:預設是私有的

Rust 是明確的公開

// 預設所有東西都是私有的
mod database {
    struct Connection {  // 私有結構體
        host: String,    // 私有欄位
    }
    
    impl Connection {
        fn connect() -> Self {  // 私有方法
            Connection {
                host: "localhost".to_string(),
            }
        }
    }
}

// 這些都無法從外部存取
// let conn = database::Connection::connect();  // ❌ 編譯錯誤

對比其他語言

# Python:所有東西預設都是公開的
class Connection:
    def __init__(self):
        self._host = "localhost"  # 慣例上的私有,但仍可存取
        
# 可以存取「私有」屬性
conn = Connection()
print(conn._host)  # 可以執行,只是不建議
// Go:大寫開頭是公開的,小寫是私有的
type connection struct {  // 私有
    host string          // 私有
}

type Connection struct {  // 公開
    Host string          // 公開
}

Rust 的可見性層級

mod outer {
    // pub:完全公開
    pub fn public_function() {}
    
    // pub(crate):在當前 crate 內公開
    pub(crate) fn crate_function() {}
    
    // pub(super):在父模組中公開
    pub(super) fn parent_function() {}
    
    // pub(in path):在指定路徑中公開
    pub(in crate::outer) fn limited_function() {}
    
    // 預設:私有
    fn private_function() {}
    
    mod inner {
        pub fn can_access_parent() {
            super::parent_function();  // ✅ 可以
            // super::private_function();  // ❌ 不行
        }
    }
}

封裝不變式:讓錯誤變得不可能

問題:公開欄位的風險

// 糟糕的設計:公開所有欄位
pub struct BankAccount {
    pub balance: i64,  // 任何人都可以修改
}

fn bad_usage() {
    let mut account = BankAccount { balance: 1000 };
    
    // 直接修改餘額,繞過所有檢查
    account.balance = -500;  // 負數餘額!
    account.balance = i64::MAX;  // 無限金錢!
}

解決方案:封裝與不變式

// 好的設計:封裝內部狀態
pub struct BankAccount {
    balance: i64,  // 私有欄位
}

impl BankAccount {
    // 建構函式確保初始狀態有效
    pub fn new(initial_balance: i64) -> Result<Self, String> {
        if initial_balance < 0 {
            return Err("初始餘額不能為負數".to_string());
        }
        Ok(BankAccount { balance: initial_balance })
    }
    
    // 只提供安全的操作
    pub fn deposit(&mut self, amount: i64) -> Result<(), String> {
        if amount <= 0 {
            return Err("存款金額必須為正數".to_string());
        }
        self.balance = self.balance.checked_add(amount)
            .ok_or("餘額溢位".to_string())?;
        Ok(())
    }
    
    pub fn withdraw(&mut self, amount: i64) -> Result<(), String> {
        if amount <= 0 {
            return Err("提款金額必須為正數".to_string());
        }
        if amount > self.balance {
            return Err("餘額不足".to_string());
        }
        self.balance -= amount;
        Ok(())
    }
    
    // 只讀存取
    pub fn balance(&self) -> i64 {
        self.balance
    }
}

// 現在無法繞過檢查
fn safe_usage() {
    let mut account = BankAccount::new(1000).unwrap();
    
    // 所有操作都經過驗證
    account.deposit(500).unwrap();
    account.withdraw(200).unwrap();
    
    // 無法直接修改餘額
    // account.balance = -500;  // ❌ 編譯錯誤
}

關鍵洞察:透過封裝,我們把「不變式」(invariant) 從文件變成了編譯器檢查。

newtype 模式:型別層級的封裝

問題:原始型別太通用

// 糟糕:容易混淆
fn transfer(from: u64, to: u64, amount: u64) {
    // from 和 to 是帳號 ID?還是金額?
    // 編譯器無法幫我們檢查
}

// 容易犯錯
transfer(100, 50, 12345);  // 參數順序錯了!

解決方案:newtype 包裝

// 用 newtype 建立語義明確的型別
pub struct AccountId(u64);
pub struct Amount(u64);

impl AccountId {
    pub fn new(id: u64) -> Self {
        AccountId(id)
    }
}

impl Amount {
    pub fn new(value: u64) -> Result<Self, String> {
        if value == 0 {
            return Err("金額不能為零".to_string());
        }
        Ok(Amount(value))
    }
    
    pub fn value(&self) -> u64 {
        self.0
    }
}

// 現在函式簽名清晰明確
fn transfer(from: AccountId, to: AccountId, amount: Amount) {
    // 型別系統確保參數不會混淆
}

// 使用時更安全
fn safe_transfer() {
    let from = AccountId::new(12345);
    let to = AccountId::new(67890);
    let amount = Amount::new(100).unwrap();
    
    transfer(from, to, amount);
    
    // 無法傳錯參數
    // transfer(amount, from, to);  // ❌ 型別不符
}

newtype 的進階應用

use std::fmt;

// 封裝敏感資訊
pub struct Password(String);

impl Password {
    pub fn new(password: String) -> Result<Self, String> {
        if password.len() < 8 {
            return Err("密碼長度至少 8 個字元".to_string());
        }
        Ok(Password(password))
    }
    
    // 提供安全的驗證方法
    pub fn verify(&self, input: &str) -> bool {
        self.0 == input
    }
}

// 防止密碼被意外印出
impl fmt::Debug for Password {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Password(***)")
    }
}

impl fmt::Display for Password {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "***")
    }
}

// 使用時更安全
fn handle_password() {
    let password = Password::new("secret123".to_string()).unwrap();
    
    // 無法直接存取內部字串
    // let s = password.0;  // ❌ 私有欄位
    
    // 印出時不會洩漏
    println!("{:?}", password);  // Password(***)
}

模組結構:組織程式碼的藝術

基本模組結構

// src/lib.rs
mod database;
mod api;
mod models;

pub use database::Connection;
pub use api::Router;
pub use models::User;

// src/database.rs
pub struct Connection {
    // 實作細節
}

impl Connection {
    pub fn new() -> Self {
        Connection {}
    }
}

// src/api.rs
pub struct Router {
    // 實作細節
}

// src/models.rs
pub struct User {
    pub id: u64,
    pub name: String,
}

分層架構

// 清晰的分層結構
mod infrastructure {
    pub(crate) mod database {
        pub(crate) struct Connection {
            // 只在 infrastructure 層可見
        }
    }
    
    pub(crate) mod cache {
        pub(crate) struct Cache {
            // 只在 infrastructure 層可見
        }
    }
}

mod domain {
    pub struct User {
        id: u64,
        name: String,
    }
    
    impl User {
        pub fn new(id: u64, name: String) -> Self {
            User { id, name }
        }
    }
}

mod application {
    use crate::domain::User;
    use crate::infrastructure::database::Connection;
    
    pub struct UserService {
        conn: Connection,
    }
    
    impl UserService {
        pub fn create_user(&self, name: String) -> User {
            // 業務邏輯
            User::new(1, name)
        }
    }
}

// 只公開最上層的 API
pub use application::UserService;
pub use domain::User;

隔離 unsafe 與同步原語

產生問題:unsafe 洩漏到使用端

// 糟糕:unsafe 暴露給使用者
pub fn bad_api() -> *mut u8 {
    unsafe {
        libc::malloc(1024) as *mut u8
    }
}

// 使用者必須處理 unsafe
fn user_code() {
    let ptr = bad_api();
    unsafe {
        *ptr = 42;  // 使用者也要寫 unsafe
    }
}

解決方案:封裝 unsafe

// 好的設計:unsafe 被封裝在內部
pub struct Buffer {
    ptr: *mut u8,
    len: usize,
}

impl Buffer {
    pub fn new(size: usize) -> Self {
        unsafe {
            let ptr = libc::malloc(size) as *mut u8;
            if ptr.is_null() {
                panic!("記憶體分配失敗");
            }
            Buffer { ptr, len: size }
        }
    }
    
    // 提供安全的 API
    pub fn write(&mut self, index: usize, value: u8) -> Result<(), String> {
        if index >= self.len {
            return Err("索引超出範圍".to_string());
        }
        unsafe {
            *self.ptr.add(index) = value;
        }
        Ok(())
    }
    
    pub fn read(&self, index: usize) -> Result<u8, String> {
        if index >= self.len {
            return Err("索引超出範圍".to_string());
        }
        unsafe {
            Ok(*self.ptr.add(index))
        }
    }
}

impl Drop for Buffer {
    fn drop(&mut self) {
        unsafe {
            libc::free(self.ptr as *mut _);
        }
    }
}

// 使用者不需要寫 unsafe
fn safe_user_code() {
    let mut buffer = Buffer::new(1024);
    buffer.write(0, 42).unwrap();
    let value = buffer.read(0).unwrap();
}

封裝同步原語

use std::sync::{Arc, Mutex};

// 糟糕:暴露鎖的細節
pub struct BadCounter {
    pub value: Arc<Mutex<i32>>,  // 使用者需要知道鎖的存在
}

// 使用者必須處理鎖
fn bad_usage() {
    let counter = BadCounter {
        value: Arc::new(Mutex::new(0)),
    };
    
    let mut guard = counter.value.lock().unwrap();
    *guard += 1;
    // 忘記釋放鎖?死鎖!
}

// 好的設計:封裝鎖的細節
pub struct Counter {
    value: Arc<Mutex<i32>>,  // 私有
}

impl Counter {
    pub fn new() -> Self {
        Counter {
            value: Arc::new(Mutex::new(0)),
        }
    }
    
    pub fn increment(&self) {
        let mut guard = self.value.lock().unwrap();
        *guard += 1;
        // guard 自動釋放
    }
    
    pub fn get(&self) -> i32 {
        let guard = self.value.lock().unwrap();
        *guard
    }
}

impl Clone for Counter {
    fn clone(&self) -> Self {
        Counter {
            value: Arc::clone(&self.value),
        }
    }
}

// 使用者不需要知道鎖的存在
fn good_usage() {
    let counter = Counter::new();
    counter.increment();
    println!("值: {}", counter.get());
}

Builder 模式:控制建構過程

問題:建構函式參數過多

// 糟糕:參數太多,容易出錯
pub struct Server {
    host: String,
    port: u16,
    timeout: u64,
    max_connections: usize,
    enable_tls: bool,
}

impl Server {
    pub fn new(
        host: String,
        port: u16,
        timeout: u64,
        max_connections: usize,
        enable_tls: bool,
    ) -> Self {
        Server { host, port, timeout, max_connections, enable_tls }
    }
}

// 使用時容易搞混參數
fn bad_usage() {
    let server = Server::new(
        "localhost".to_string(),
        8080,
        30,
        100,
        true,
    );
}

解決方案:Builder 模式

// 好的設計:Builder 模式
pub struct Server {
    host: String,
    port: u16,
    timeout: u64,
    max_connections: usize,
    enable_tls: bool,
}

pub struct ServerBuilder {
    host: String,
    port: u16,
    timeout: u64,
    max_connections: usize,
    enable_tls: bool,
}

impl ServerBuilder {
    pub fn new() -> Self {
        ServerBuilder {
            host: "localhost".to_string(),
            port: 8080,
            timeout: 30,
            max_connections: 100,
            enable_tls: false,
        }
    }
    
    pub fn host(mut self, host: impl Into<String>) -> Self {
        self.host = host.into();
        self
    }
    
    pub fn port(mut self, port: u16) -> Self {
        self.port = port;
        self
    }
    
    pub fn timeout(mut self, timeout: u64) -> Self {
        self.timeout = timeout;
        self
    }
    
    pub fn max_connections(mut self, max: usize) -> Self {
        self.max_connections = max;
        self
    }
    
    pub fn enable_tls(mut self, enable: bool) -> Self {
        self.enable_tls = enable;
        self
    }
    
    pub fn build(self) -> Result<Server, String> {
        // 驗證配置
        if self.port == 0 {
            return Err("埠號不能為 0".to_string());
        }
        
        Ok(Server {
            host: self.host,
            port: self.port,
            timeout: self.timeout,
            max_connections: self.max_connections,
            enable_tls: self.enable_tls,
        })
    }
}

// 使用時清晰明確
fn good_usage() {
    let server = ServerBuilder::new()
        .host("0.0.0.0")
        .port(8080)
        .timeout(60)
        .enable_tls(true)
        .build()
        .unwrap();
}

總結:API 設計的減法哲學

1. 預設私有,明確公開

// 只公開必要的東西
pub struct Public {
    private_field: i32,  // 預設私有
}

impl Public {
    pub fn public_method(&self) {}  // 明確公開
    fn private_method(&self) {}     // 預設私有
}

2. 用型別封裝不變式

// 讓錯誤的使用方式變得不可能
pub struct ValidatedEmail(String);

impl ValidatedEmail {
    pub fn new(email: String) -> Result<Self, String> {
        if !email.contains('@') {
            return Err("無效的電子郵件".to_string());
        }
        Ok(ValidatedEmail(email))
    }
}

3. 隔離危險操作

// unsafe 和鎖應該被封裝在內部
pub struct SafeWrapper {
    inner: UnsafeInner,  // 私有
}

impl SafeWrapper {
    pub fn safe_operation(&self) {
        // 內部處理 unsafe
    }
}

4. 最小化公開表面

// 只公開使用者真正需要的東西
mod internal {
    pub(crate) fn helper() {}  // 只在 crate 內可見
}

pub fn public_api() {
    internal::helper();  // 內部使用
}

:好的 API 設計不是提供更多功能,而是讓使用者只能用正確的方式使用。

在下一篇中,我們將探討 進階智慧指標,看看 Pin 和特徵物件如何處理更複雜的所有權場景。

相關連結與參考資源


上一篇
(Day24) Rust 錯誤處理進階:thiserror、anyhow 與邊界策略
下一篇
(Day26) Rust 進階智慧指標:Pin、特徵物件與動態派發的界線
系列文
Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言